#include <cad-slicer/slicer.h>

#include <fstream>
#include <iostream>

#include <assimp/Importer.hpp>
#include <assimp/Exporter.hpp>
#include <assimp/postprocess.h>

#include <assimp/scene.h>
#include <assimp/mesh.h>

#include <prenstar/assert.h>
#include <cad-slicer/load.h>
#include <cad-slicer/object-transform.h>

#include "output.h"


std::size_t get_matching_format(
    std::string const& outf,
    Assimp::Exporter const& exporter)
{
  for (size_t i = 0, end = exporter.GetExportFormatCount();
       i < end; ++i)
  {
    const aiExportFormatDesc* const e = exporter.GetExportFormatDescription(i);
    if (outf == e->id) return i;
  }

  return std::numeric_limits<std::size_t>::max();
}


namespace cadslicer
{


slicer::slicer(void) {}


slicer::slicer(const cadslicer::slicer::parameters& param)
  : slicer()
{ set_parameters(param); }


slicer::slicer(const std::string& param_file)
  : slicer()
{ set_parameters(param_file); }


slicer::~slicer(void) {}


void slicer::operator ()(Assimp::Importer& importer)
{
  importer.ApplyPostProcessing(
        _slicer_parameters.post_process |
        aiProcess_Triangulate |
        aiProcess_JoinIdenticalVertices);

  aiScene const* object = importer.GetScene();

  repair_vertice(importer, _slicer_parameters.step * 1e-3);

  repair_faces(object);

  // Slicing.
  if (find_object_dimensions(object))
  {
    unsigned number_of_slice =
        1 + (unsigned long) (
          _slicer_parameters.slicing_box_dimensions[2] /
        _slicer_parameters.step);

    // Init slices.
    {
      _slices = std::vector<slice>(number_of_slice);

      unsigned idx = 0;
      float z = 0.f;
      float size_between_slice = _slicer_parameters.step;
      for (slice& sl : _slices)
      {
        sl.set_z(z);
        sl.init_faces();
        ++idx;
        z = idx * size_between_slice;
      }
    }

    print_object_properties(object);

    float step_meter_inv = 1.f / _slicer_parameters.step;

    if (((_obj_prop.object_x_max - _obj_prop.object_x_min) <=
         _slicer_parameters.slicing_box_dimensions[0]) &&
        ((_obj_prop.object_y_max - _obj_prop.object_y_min) <=
         _slicer_parameters.slicing_box_dimensions[1]) &&
        ((_obj_prop.object_z_max - _obj_prop.object_z_min) <=
         _slicer_parameters.slicing_box_dimensions[2]))
    {
      aiMesh const* const* current_mesh = object->mMeshes;
      aiMesh const* const* const mesh_end = current_mesh + object->mNumMeshes;

      // Find slice frontier for each face of each mesh.
      for (; current_mesh != mesh_end; ++current_mesh)
      {
        aiFace const* const face_end =
            (*current_mesh)->mFaces + (*current_mesh)->mNumFaces;

        for (aiFace const* current_face = (*current_mesh)->mFaces;
             current_face != face_end; ++current_face)
        {
          unsigned const* const vertex_end =
              current_face->mIndices + current_face->mNumIndices;

          float max_z = -INFINITY, min_z = +INFINITY;

          if (!current_face->mNumIndices) continue;
          prenstar_assert_eq(current_face->mNumIndices, 3);

          for (unsigned const* vertex = current_face->mIndices;
               vertex != vertex_end; ++vertex)
          {
            aiVector3D const& current_vertex =
                (*current_mesh)->mVertices[*vertex];

            max_z = std::max(max_z, current_vertex.z);
            min_z = std::min(min_z, current_vertex.z);
          }

          prenstar_assert_le(max_z, _obj_prop.object_z_max);
          prenstar_assert_ge(min_z, _obj_prop.object_z_min);

          unsigned slice_min = (min_z - _obj_prop.object_z_min) * step_meter_inv;
          unsigned slice_max = (max_z - _obj_prop.object_z_min) * step_meter_inv;

          prenstar_assert_lt(slice_min, _slices.size());
          prenstar_assert_lt(slice_max, _slices.size());

          prenstar_assert_ge(min_z - _obj_prop.object_z_min, _slices.at(slice_min).get_z());
          prenstar_assert_le(min_z - _obj_prop.object_z_min, _slices.at(slice_min+1).get_z());

          prenstar_assert_ge(max_z - _obj_prop.object_z_min, _slices.at(slice_max).get_z());
          prenstar_assert_le(max_z - _obj_prop.object_z_min, _slices.at(slice_max+1).get_z());

          if (slice_min != slice_max)
          {
            _slices.at(slice_min).add_face(*current_mesh, current_face);
            _slices.at(slice_max).remove_face(*current_mesh, current_face);
          }
        }
      }

      slice_object();
    }
    else
      throw std::runtime_error("Object doesn't fit bed dimension.");
  }
  else
    throw std::runtime_error("This is an empty object.");
}


void slicer::operator ()(const std::string& input_file)
{
  Assimp::Importer importer;
  importer.ReadFile(input_file, 0);

  return operator ()(importer);
}


bool slicer::set_parameters(const slicer::parameters& param)
{
  /// @todo Set check tests.
  _slicer_parameters = param;

  return true;
}


bool slicer::set_parameters(const std::string& param_file)
{
  std::ifstream file(param_file);

  if (file.is_open())
  {
    slicer::parameters p;
    if (load(&file, &p)) return set_parameters(p);
  }

  return false;
}


void slicer::check_parameters(void) const
{
  if (_slicer_parameters.step == 0.f ||
      _slicer_parameters.slicing_box_dimensions[0] == 0.f ||
      _slicer_parameters.slicing_box_dimensions[1] == 0.f ||
      _slicer_parameters.slicing_box_dimensions[2] == 0.f)
    throw std::runtime_error("You have to set parameters before using slicer.");
}


bool slicer::find_object_dimensions(const void* obj)
{
  prenstar_assert_not_nullptr(obj);

  aiScene const* object = reinterpret_cast<aiScene const*>(obj);

  if (object->HasMeshes())
  {
    _obj_prop.object_x_max = _obj_prop.object_y_max =
        _obj_prop.object_z_max = -INFINITY;
    _obj_prop.object_x_min = _obj_prop.object_x_min =
        _obj_prop.object_z_min = +INFINITY;

    aiMesh** meshes = object->mMeshes;
    aiMesh** const end = meshes + object->mNumMeshes;

    for (aiMesh** current_mesh = meshes; current_mesh != end; ++current_mesh)
    {
      aiVector3D const* vert_max =
          (*current_mesh)->mVertices + (*current_mesh)->mNumVertices;
      for (aiVector3D const* vert = (*current_mesh)->mVertices;
           vert < vert_max; ++vert)
      {
        _obj_prop.object_x_max = std::max(vert->x, _obj_prop.object_x_max);
        _obj_prop.object_y_max = std::max(vert->y, _obj_prop.object_y_max);
        _obj_prop.object_z_max = std::max(vert->z, _obj_prop.object_z_max);

        _obj_prop.object_x_min = std::min(vert->x, _obj_prop.object_x_min);
        _obj_prop.object_y_min = std::min(vert->y, _obj_prop.object_y_min);
        _obj_prop.object_z_min = std::min(vert->z, _obj_prop.object_z_min);
      }
    }

    return true;
  }

  return false;
}


void slicer::print_object_properties(const void* obj)
{
  prenstar_assert_not_nullptr(obj);

  aiScene const* object = reinterpret_cast<aiScene const*>(obj);

  aiMesh** meshes = object->mMeshes;
  aiMesh** const end = meshes + object->mNumMeshes;

  std::cout << "Number of slices: " << _slices.size() << std::endl;

  std::cout << "Dimensions:\n  x: "
            << _obj_prop.object_x_min << " -> " << _obj_prop.object_x_max << "\n  y: "
            << _obj_prop.object_y_min << " -> " << _obj_prop.object_y_max << "\n  z: "
            << _obj_prop.object_z_min << " -> " << _obj_prop.object_z_max
            << "\nMeshes number: " << object->mNumMeshes << std::endl;
  int mesh = 0;

  for (aiMesh** current_mesh = meshes; current_mesh != end; ++current_mesh, ++mesh)
  {
    std::cout << "For mesh n°" << mesh << ':' << std::endl;
    std::cout << "  Number of faces:    " << (*current_mesh)->mNumFaces << std::endl;
    std::cout << "  Number of bones:    " << (*current_mesh)->mNumBones << std::endl;
    std::cout << "  Number of vertices: " << (*current_mesh)->mNumVertices << std::endl;
  }
}


void slicer::slice_object(void)
{
  faces faces_of_the_slice;
  prenstar_assert_true(faces_of_the_slice.empty());

  for (slice& sl : _slices)
  {
    sl.update_faces_for_current(faces_of_the_slice);

    sl.init_edges(faces_of_the_slice.size());

    for (face_id const& fid : faces_of_the_slice)
    {
      aiMesh const* mesh = reinterpret_cast<aiMesh const*>(fid.first);
      aiFace const* face = reinterpret_cast<aiFace const*>(fid.second);

      prenstar_assert_not_nullptr(mesh);
      prenstar_assert_not_nullptr(face);
      prenstar_assert_eq(face->mNumIndices, 3);

      aiVector3D const* A = &mesh->mVertices[face->mIndices[0]];
      aiVector3D const* B = &mesh->mVertices[face->mIndices[1]];
      aiVector3D const* C = &mesh->mVertices[face->mIndices[2]];

      insert_in_slice(sl, A,B,C);
    }

    for (triangle const& tr : _filled_holls)
      insert_in_slice(sl, tr[0], tr[1], tr[2]);

    sl.finish_edges();

    sl.update_faces_for_next(faces_of_the_slice);

    sl.finish_faces();

    if (_post_slice) _post_slice(sl);
  }
}


void slicer::repair_faces(const void* object)
{
  aiScene const* obj = reinterpret_cast<aiScene const*>(object);

  aiMesh const* const* current_mesh = obj->mMeshes;
  aiMesh const* const* const mesh_end = current_mesh + obj->mNumMeshes;

  typedef std::set<vertex_id> vertex_neighbour;
  typedef std::vector<vertex_id> contour3d;

  // Find invalid edges.
  std::map<edge_id, std::forward_list<std::pair<mesh,face>>> edge_to_triangle;
  std::map<vertex_id, vertex_neighbour> adjacent_vertex;
  std::set<edge_id> invalid_edges;
  std::set<edge_id> empty_surface;
  {
    std::map<edge_id, unsigned> edge_count;
    for (; current_mesh != mesh_end; ++current_mesh)
    {
      aiFace const* const face_end =
          (*current_mesh)->mFaces + (*current_mesh)->mNumFaces;

      for (aiFace const* current_face = (*current_mesh)->mFaces;
           current_face != face_end; ++current_face)
      {
        prenstar_assert_eq(current_face->mNumIndices, 3);

        edge_id triangle[3] =
        {
          edge_id(
          &(*current_mesh)->mVertices[current_face->mIndices[0]],
          &(*current_mesh)->mVertices[current_face->mIndices[1]],
          sort_tag()),
          edge_id(
          &(*current_mesh)->mVertices[current_face->mIndices[1]],
          &(*current_mesh)->mVertices[current_face->mIndices[2]],
          sort_tag()),
          edge_id(
          &(*current_mesh)->mVertices[current_face->mIndices[2]],
          &(*current_mesh)->mVertices[current_face->mIndices[0]],
          sort_tag())
        };

        for (edge_id const& id : triangle)
        {
          if (edge_count.count(id))
            ++edge_count[id];
          else
            edge_count[id] = 1;

          edge_to_triangle[id].push_front(
                std::make_pair(*current_mesh, current_face));
        }
      }
    }

    for (std::pair<edge_id, unsigned> const& count : edge_count)
    {
      prenstar_assert_lt(count.second,4);

      if (count.second != 2) /* 1 or 3 */
      {
        adjacent_vertex[count.first.ids[0]].insert(count.first.ids[1]);
        adjacent_vertex[count.first.ids[1]].insert(count.first.ids[0]);

        invalid_edges.insert(count.first);
      }
      if (count.second == 3)
        empty_surface.insert(count.first);
    }
  }

  std::set<edge_id> visited;
  // Remove empty faces.
  {
    std::vector<std::set<edge_id>> to_remove;
    to_remove.reserve(empty_surface.size());

    // Find floating surfaces.
    for (edge_id const& edg : empty_surface)
    {
      if (visited.count(edg)) continue;
      visited.insert(edg);

      prenstar_assert_true(adjacent_vertex.count(edg.ids[0]));
      prenstar_assert_true(adjacent_vertex.count(edg.ids[1]));

      std::set<edge_id> current;

      vertex_id first = edg.ids[0];
      current.insert(edg);

      vertex_id current_vertex = first;
      vertex_id new_vertex = edg.ids[1];

      while (new_vertex != first)
      {
        auto it = adjacent_vertex.find(new_vertex);

        prenstar_assert_false(it == adjacent_vertex.end());
        current_vertex = new_vertex;

        auto itv = it->second.begin(), itvEnd = it->second.end();
        while (itv != itvEnd &&
               visited.count(edge_id(current_vertex, *itv, sort_tag())))
          ++itv;

        edge_id e(current_vertex, *itv, sort_tag());
        visited.insert(e);

        prenstar_assert_false(itv == itvEnd);
        new_vertex = *itv;
        prenstar_assert_false(new_vertex == current_vertex);

        prenstar_assert_true(edge_to_triangle.count(e));
        current.insert(e);
      }

      to_remove.push_back(current);
    }

    std::cout << "To remove:\n";
    for (std::set<edge_id> const& ctr : to_remove)
    {
      for (edge_id const& e : ctr)
      {
        prenstar_assert_true(edge_to_triangle.count(e));
        std::cout << e << ' ';
      }
      std::cout << std::endl;
    }

    for (std::set<edge_id>& edges : to_remove)
    {
      std::set<aiFace*> deleted;
      while (true)
      {
        auto it = edges.begin();
        while (it != edges.end() && empty_surface.count(*it))
          ++it;

        if (it == edges.end()) break;

        prenstar_assert_true(edge_to_triangle.count(*it));

        std::pair<mesh,face>& where = edge_to_triangle[*it].front();
        aiMesh* m = const_cast<aiMesh*>(reinterpret_cast<aiMesh const*>(where.first));
        aiFace* f = const_cast<aiFace*>(reinterpret_cast<aiFace const*>(where.second));

        if (deleted.count(f))
        {
          edges.erase(*it);
          continue;
        }

        deleted.insert(f);

        edge_id eds[3] =
        {
          edge_id(
            &m->mVertices[f->mIndices[0]],
            &m->mVertices[f->mIndices[1]], sort_tag()),
          edge_id(
            &m->mVertices[f->mIndices[1]],
            &m->mVertices[f->mIndices[2]], sort_tag()),
          edge_id(
            &m->mVertices[f->mIndices[0]],
            &m->mVertices[f->mIndices[2]], sort_tag())
        };

        for (edge_id const& e : eds)
        {
          edges.insert(e);
          prenstar_assert_true(edge_to_triangle.count(e));
        }

        std::cout << "Remove triangle: "
                  << m->mVertices[f->mIndices[0]] << ' '
                  << m->mVertices[f->mIndices[1]] << ' '
                  << m->mVertices[f->mIndices[2]] << std::endl;

        f->mNumIndices = 0;

        edges.erase(*it);
      }
    }
  }

  // Fill holls.
  {
    std::vector<std::vector<vertex_id>> to_fill;
    to_fill.reserve(empty_surface.size());

    // Find holls.
    for (edge_id const& edg : invalid_edges)
    {
      if (visited.count(edg)) continue;
      visited.insert(edg);

      prenstar_assert_true(adjacent_vertex.count(edg.ids[0]));
      prenstar_assert_true(adjacent_vertex.count(edg.ids[1]));

      std::vector<vertex_id> current;
      current.reserve(invalid_edges.size());

      vertex_id first = edg.ids[0];
      current.push_back(first);

      vertex_id current_vertex = first;
      vertex_id new_vertex = edg.ids[1];

      while (new_vertex != first)
      {
        auto it = adjacent_vertex.find(new_vertex);

        prenstar_assert_false(it == adjacent_vertex.end());
        current_vertex = new_vertex;

        auto itv = it->second.begin(), itvEnd = it->second.end();
        while (itv != itvEnd &&
               visited.count(edge_id(current_vertex, *itv, sort_tag())))
          ++itv;

        edge_id e(current_vertex, *itv, sort_tag());
        visited.insert(e);

        prenstar_assert_false(itv == itvEnd);
        new_vertex = *itv;
        prenstar_assert_false(new_vertex == current_vertex);

        current.push_back(current_vertex);
      }

      to_fill.push_back(current);
    }

    std::cout << "To fill:\n";
    for (std::vector<vertex_id> const& ctr : to_fill)
    {
      for (vertex_id const& v : ctr)
        std::cout << (*reinterpret_cast<aiVector3D const*>(v)) << ' ';
      std::cout << std::endl;
    }

    // Fill them
    {
      for (std::vector<vertex_id> const& ctr : to_fill)
      {
        unsigned left = 0;
        unsigned right = ctr.size() - 1;
        bool on_left = true;

        while ((left + 1) != right)
        {
          if (on_left)
          {
            _filled_holls.push_front(
                  triangle{{ctr[left], ctr[left+1], ctr[right]}});
            ++left;
          }
          else
          {
            _filled_holls.push_front(
                  triangle{{ctr[left], ctr[right], ctr[right-1]}});
            --right;
          }

          on_left = !on_left;
        }
      }
    }
  }
}


inline void
slicer::insert_in_slice(slice& sl, vertex_id a, vertex_id b, vertex_id c)
{
  auto A = reinterpret_cast<aiVector3D const*>(a);
  auto B = reinterpret_cast<aiVector3D const*>(b);
  auto C = reinterpret_cast<aiVector3D const*>(c);

  float plane_z = sl.get_z() + _obj_prop.object_z_min;

  auto ok = sl.has_face(A,B,C);
  if (!ok.first)
  {
    aiVector3D const* under[3];
    aiVector3D const* upper[3];
    aiVector3D const** uniq;
    aiVector3D const** dual;

    unsigned char under_nb = 0, upper_nb = 0;

    if (A->z > plane_z)
      upper[upper_nb++] = A;
    else
      under[under_nb++] = A;

    if (B->z > plane_z)
      upper[upper_nb++] = B;
    else
      under[under_nb++] = B;

    if (C->z > plane_z)
      upper[upper_nb++] = C;
    else
      under[under_nb++] = C;

    /**/ if (upper_nb == 2 && under_nb == 1)
    {
      uniq = under;
      dual = upper;
    }
    else if (upper_nb == 1 && under_nb == 2)
    {
      uniq = upper;
      dual = under;
    }
    else
    {
      // Happen when a triangle is exactly between
      // two consecutive slices or when from fixed holl.
      return;
    }

    // z0 = current_z + _object_z_min
    // O = (_object_x_min, _object_y_min, _object_z_min)
    // X + O = uniq + (dual[0] - uniq) * (z0 - uniq.z) / (dual[0].z - uniq.z)
    // Y + O = uniq + (dual[1] - uniq) * (z0 - uniq.z) / (dual[1].z - uniq.z)

    float coeff  = plane_z - uniq[0]->z;
    float coeffX = coeff / (dual[0]->z - uniq[0]->z);
    float coeffY = coeff / (dual[1]->z - uniq[0]->z);

    sl.add_edge(
        /* Pi(X, P(z=plane_z)) = */ point2f(
          uniq[0]->x - _obj_prop.object_x_min + coeffX * (dual[0]->x - uniq[0]->x),
          uniq[0]->y - _obj_prop.object_y_min + coeffX * (dual[0]->y - uniq[0]->y)),
        /* Pi(Y, P(z=plane_z)) = */ point2f(
          uniq[0]->x - _obj_prop.object_x_min + coeffY * (dual[1]->x - uniq[0]->x),
          uniq[0]->y - _obj_prop.object_y_min + coeffY * (dual[1]->y - uniq[0]->y)),
        triangle_id(uniq[0],dual[0],dual[1]), std::move(ok.second));
  }
}


} // cadslicer
